package it.sevenbits.app.database;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;

import it.sevenbits.app.journal.Journal;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

import it.sevenbits.app.parser.CmdParser;
import it.sevenbits.app.parser.CsvParser;

/**
 * In-memory database
 * 
 * @author 7bits
 * 
 */
public class InMemDatabase implements it.sevenbits.app.database.DatabaseInterface {

    private int nodes;
    private String DbName;
    private File[] CsvFile;
    private ArrayList<ArrayList<String[]>> MemDb;
    private final static Logger LOG = Logger.getLogger(InMemDatabase.class);
    private final static Journal journal = new Journal("log/");

    /*
     * Constructor
     */
    public InMemDatabase() {
        PropertyConfigurator.configure("log4j.properties");
        LOG.debug("New InMemDatabase instance");
    }

    /**
     * Initialization
     * @param DbName
     * @param N partitions
     */
    @SuppressWarnings("unchecked")
    private void init(String DbName, int N) {

        LOG.debug("init(" + DbName + ", " + Integer.toString(N) + ") started");
        this.DbName = DbName;
        nodes = N;

        MemDb = new ArrayList<ArrayList<String[]>>();
        for (int i = 0; i < this.nodes; ++i) {
            ArrayList<String[]> node = new ArrayList<String[]>();
            MemDb.add((ArrayList<String[]>) node.clone());
        }
        CsvFile = new File[nodes];
        MemDb.trimToSize();
        int i = 0;
        for (File f : CsvFile) {
            f = new File(this.DbName + ".node[" + i + "]");
            CsvFile[i] = f;
            if (f.exists()) {
                initFromFile(f, MemDb.get(i));
            } else {
                try {
                    f.createNewFile();
                } catch (IOException ex) {
                    LOG.error("File creation failure: " + f.getName() + " "
                        + ex.getMessage());
                    return;
                }
            }
            ++i;
        }
        LOG.debug("init(" + DbName + ", " + Integer.toString(N) + ") finished");
    }

    /**
     * Initializes node [list] from file [f]
     * @param f
     * @param list
     */
    private void initFromFile(File f, ArrayList<String[]> list) {
        try {
            LOG.debug("initFromFile(" + f.getName() + ") started");
            BufferedReader in = new BufferedReader(new FileReader(f));
            CsvParser myCsv = new CsvParser();
            String str;
            String[] row;

            while ((str = in.readLine()) != null) {
                row = myCsv.parse(str);
                for (int i = 0; i < row.length; ++i) {
                    if ((row[i] != null) && (row[i].length() >= 2)) {
                        if (row[i].startsWith("\"") && row[i].endsWith("\"")) {
                            row[i] = row[i].substring(1, row[i].length() - 1);
                            row[i] = row[i].replaceAll("(\"\")", "\"");
                        }
                    }
                }
                list.add((String[]) row.clone());
            }
            in.close();
        } catch (IOException ex) {
            LOG.error("initFromFile() IOException: " + ex.getMessage());
            return;
        }
        LOG.debug("initFromFile(" + f.getName() + ") finished");
    }

    /**
     * Dumps database in file
     */
    private void dump() {

        LOG.debug("dump() started");
        for (int i = 0; i < this.MemDb.size(); ++i) {
            dumpInFile(this.CsvFile[i], this.MemDb.get(i));
        }
        LOG.debug("dump() finished");
    }

    /**
     * Dumps node [list] in file [f]
     * @param f
     * @param list
     */
    private void dumpInFile(File f, ArrayList<String[]> list) {

        try {
            LOG.debug("dumpInFile(f, list) started: " + f.getName());
            if (list.size() == 0) {
                return;
            }

            PrintWriter out = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream(f)));

            for (int i = 0; i < list.size(); ++i) {
                String[] row = list.get(i);
                for (int j = 0; j < row.length; ++j) {
                    String cell = row[j];
                    String str = String.valueOf(cell);
                    str = str.replace("\"", "\"\"");
                    str = str.replace("\n", "");
                    if (str.contains(",") || str.contains(" ")
                        || str.contains("\"")) {
                        out.write("\"");
                        out.write(str);
                        out.write("\"");
                    } else {
                        out.write(str);
                    }
                    if (j != (row.length - 1)) {
                        out.write(",");
                    }
                }
                out.write("\n");
            }
            out.close();
        } catch (Exception ex) {
            LOG.error("dumpInFile exception: " + ex.getMessage());
            return;
        }
        LOG.debug("dumpInFile(f, list) finised: " + f.getName());
    }

    /**
     * Returns databases name
     * @return
     */
    private String dbName() {

        LOG.debug("dbName(): returned DB name");
        return String.valueOf(this.DbName);
    }

    /*
     * CRUD
     */
    
    /**
     * Inserts record [record] with primary key [PKey]
     * @param PKey
     * @param record
     */
    private void create(String PKey, String[] record) {

        LOG.debug("create(" + PKey + ", record) started");
        if (PKey == null) {
            LOG.debug("create(): null primary key");
            return;
        }
        int node = Math.abs(PKey.hashCode()) % this.nodes;
        for (String[] row : this.MemDb.get(node)) {
            if (row[0].compareTo(PKey) == 0) {
                LOG.debug("create(): record with PK = " + PKey
                    + " already exists");
                return;
            }
        }
        String[] newRow = new String[record.length + 1];
        newRow[0] = PKey;
        System.arraycopy(record, 0, newRow, 1, record.length);
        this.MemDb.get(node).add((String[]) newRow.clone());
        LOG.debug("create(" + PKey + ", record) finished");
    }

    /**
     * Finds record with primary key [PKey]
     * @param PKey
     * @return
     */
    private String[] retrieve(String PKey) {

        LOG.debug("retrieve(" + PKey + ") started");
        int node = Math.abs(PKey.hashCode()) % this.nodes;
        for (String[] row : this.MemDb.get(node)) {
            if (row[0].compareTo(PKey) == 0) {
                LOG.debug("retrieve(" + PKey + ") finished");
                return ((String[]) row.clone());
            }
        }
        LOG.debug("retrieve(): no value for " + PKey + "found");
        LOG.debug("retrieve(" + PKey + ") finished");
        return null;
    }

    /**
     * Changes record value to [record] matching primary key [PKey]
     * @param PKey
     * @param record
     */
    private void update(String PKey, String[] record) {

        LOG.debug("update(" + PKey + ", record) started");
        int node = Math.abs(PKey.hashCode()) % this.nodes;
        for (int i = 0; i < this.MemDb.get(node).size(); ++i) {
            if (this.MemDb.get(node).get(i)[0].compareTo(PKey) == 0) {
                String[] newRow = new String[record.length + 1];
                newRow[0] = PKey;
                System.arraycopy(record, 0, newRow, 1, record.length);
                this.MemDb.get(node).remove(i);
                this.MemDb.get(node).add(i, newRow);
                LOG.debug("update(" + PKey + ", record) started");
                return;
            }
        }
        LOG.debug("update(): no value for " + PKey + "found");
        LOG.debug("update(" + PKey + ", record) finished");
    }

    /**
     * Erases record with primary key [PKey] 
     * @param PKey
     */
    private void delete(String PKey) {

        LOG.debug("delete(" + PKey + ", record) started");
        int node = Math.abs(PKey.hashCode()) % this.nodes;
        for (int i = 0; i < this.MemDb.get(node).size(); ++i) {
            if (this.MemDb.get(node).get(i)[0].compareTo(PKey) == 0) {
                this.MemDb.get(node).remove(i);
                LOG.debug("delete(" + PKey + ", record) finished");
                return;
            }
        }
        LOG.debug("delete(): no value for " + PKey + "found");
        LOG.debug("delete(" + PKey + ", record) finished");
    }

    /**
     * Clears all nodes
     */
    private void clean() {

        LOG.debug("clean() started");
        for (int i = 0; i < this.nodes; ++i) {
            this.MemDb.get(i).clear();
        }
        LOG.debug("clean() finished");
    }
    
    /**
     * @return all records
     */
    private String[] all() {
    	ArrayList<String> records = new ArrayList<String>();
    	for (ArrayList<String[]> node : this.MemDb) {
    		for (String[] record : node) {
    			String temp = "";
    			for (String cell : record) {
    				temp += cell + "\t";
    			}
    			records.add(temp);
    		}
    	}
    	String[] r = new String[records.size()];
    	r = records.toArray(r);
    	return r;
    }

    /**
     * Executes query [command] and
     * returns records if there are any
     * @param command
     */
    public String[] execute(String command) {

        CmdParser parser = new CmdParser();
        String[] result = parser.parse(command);
        String[] response = null;
        if (result[0].toLowerCase().startsWith("init")) {
            if (result.length < 3) {
                LOG.warn("not enough parameters");
                return null;
            }
            this.init(result[1], Integer.parseInt(result[2]));
        } else if (result[0].toLowerCase().startsWith("dump")) {
            this.dump();
        } else if (result[0].toLowerCase().startsWith("clean")) {
            this.clean();
        } else if (result[0].toLowerCase().startsWith("dbname")) {
            response = new String[] {
                this.dbName()
            };
        } else if (result[0].toLowerCase().startsWith("retrieve")) {
            if (result.length < 2) {
                LOG.warn("not enough parameters");
                return null;
            }
            response = this.retrieve(result[1]);
        } else if (result[0].toLowerCase().startsWith("delete")) {
            if (result.length < 2) {
                LOG.warn("not enough parameters");
                return null;
            }
            this.delete(result[1]);
        } else if (result[0].toLowerCase().startsWith("update")) {
            if (result.length < 3) {
                LOG.warn("not enough parameters");
                return null;
            }
            String[] record = null;
            System.arraycopy(result, 2, record, 0, result.length - 2);
            this.update(result[1], record);
        } else if (result[0].toLowerCase().startsWith("create")) {
            if (result.length < 3) {
                LOG.warn("not enough parameters");
                return null;
            }
            String[] record = new String[result.length - 2];
            System.arraycopy(result, 2, record, 0, result.length - 2);
            this.create(result[1], record);
        } else if (result[0].toLowerCase().startsWith("show")) {
            response = this.all();
        } else {
            LOG.warn("unknown command: " + command);
            return null;
        }
        journal.put(command);
        return response;
    }

}
